import yaml
from soaputils import envelope, make_fault

__all__ = ['InvalidSettingException', 'SimulatorSettings', 'Operation',
           'Reaction']

def get_required(key, dict_):
    """Retrieves a required key from the dict.  If not found, an 
    InvalidSettingException is raised."""
    try:
        return dict_[key]
    except KeyError:
        raise InvalidSettingsException("Missing %s in %s" %
                (key, dict_))

class InvalidSettingsException(Exception):
    """Raised when the settings file is malformed or if a field holds
    an illegal value"""
    pass

class Reaction(object):
    """A potential reaction for an operation, consisting of 
    a key and a response"""

    def __init__(self, reaction):
        self.reaction = reaction
    
    @property
    def key(self):
        """The key to search for in the request body"""
        return str(get_required('key', self.reaction))

    @property
    def response(self):
        """The response to return to the client.  May need to be
        wrapped in a SOAP envelope"""
        return get_required('response', self.reaction).strip()

    def matches(self, request):
        """Returns true if the key is found in the request body"""
        return self.key in request

    def __str__(self):
        return "key: %s response: %s" % (self.key, self.response)

class Operation(object):
    """Holds the data related to a particular web service operation
    identified by its SOAPAction"""

    def __init__(self, op):
        self.op = op

    def __getattr__(self, attr):
        """Try to access the attribute from the underlying yaml object"""
        return get_required(attr, self.op)

    @property
    def delay(self):
        """Time in seconds to wait before responding"""
        return int(self.op.get('delay', 0))

    @property
    def soap_version(self):
        return str(self.op.get('soap_version', '1.1'))

    @property
    def wrap(self):
        """Returns a boolean to indicate whether the response should
        be wrapped in the SOAP Envelope.  Default is True."""
        result = self.op.get('wrap', True)
        if isinstance(result, basestring):
            result = True if "T" in result.upper() else False
        return result

    @property
    def default_response(self):
        """The response that should be returned if no matching
        reaction is found"""
        return get_required('default_response', self.op).strip()

    @property
    def reactions(self):
        """The reactions defined for this operation"""
        return map(Reaction, self.op.get('reactions', []))

    def get_response(self, request):
        """Returns the response from a matching reaction or 
        the default response otherwise. If wrap is True, 
        the message will be wrapped in a default SOAP Envelope"""
        response = self.default_response
        for reaction in self.reactions:
            if reaction.matches(request):
                response = reaction.response
                break
        ver = self.soap_version
        return envelope(response, ver) if self.wrap else response

    def __str__(self):
        return self.soapaction

class SimulatorSettings(object):

    def __init__(self, config_file):
        self.config_file = open(config_file)
        self.reload()

    def __getattr__(self, attr):
        """Try to access the attribute from the underlying yaml object"""
        return get_required(attr, self.settings)

    @property
    def port(self):
       """Port number to host the server. Returns an int"""
       return int(get_required('port', self.settings))

    @property
    def operations(self):
        """Operations supported by this simulator"""
        return map(Operation, get_required('operations', self.settings))

    def get_operation(self, soapaction):
        """Returns the operation object for the given SOAPAction.
        Returns None if no matching operation is found"""
        for op in self.operations:
            if op.soapaction == soapaction:
                return op
        return None

    def dump(self):
        """Returns the configuration settings as YAML string"""
        return yaml.dump(self.settings)

    def reload(self):
        """Reloads the settings from the config file"""
        self.config_file.seek(0)
        self.settings = yaml.load(self.config_file.read())

    def __str__(self):
        return "%s on port %d" % (self.name, self.port)
